Skip to content

feat(tools,forks): Extend EEST to support EIP-7928 payload #1866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: feat/block-access-list
Choose a base branch
from

Conversation

raxhvl
Copy link
Member

@raxhvl raxhvl commented Jul 7, 2025

🗒️ Description

A sub-PR of #205 that introduces following changes to the framework:

  1. Adds a new fork called BlockAccessLists
  2. Adds bal_hash to block header
  3. Adds block_access_lists to block body
  4. Adds bal_hash and block_access_lists to t8n environment, and test fixtures.

Implementation of EIP-7928 engine API specs

The implementation of EIP-7928 requires passing of the ssz encoded block access list to be passed as a parameter of engine API specifications. The framework's engine API specs has been updated based on the proposed update to engine API specs.

References

🔗 Related Issues or PRs

closes #1836

✅ Checklist

  • All: Ran fast tox checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    uvx --with=tox-uv tox -e lint,typecheck,spellcheck,markdownlint
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered adding an entry to CHANGELOG.md.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).

@raxhvl
Copy link
Member Author

raxhvl commented Jul 7, 2025

Unsure what this error is about:

src/ethereum_test_forks/forks/transition.py:8: error: Only concrete class can be given where "type[BaseFork]" is expected  [type-abstract]

@raxhvl raxhvl added the scope:tools Scope: Changes ethereum_test_tools package label Jul 7, 2025
@raxhvl
Copy link
Member Author

raxhvl commented Jul 7, 2025

I haven't modified the engine API payload yet because there are no specifications for it. I can add it if it would help clients prototype more effectively.

cc: @nerolation - We need to update the Engine API specs once the EIP is SFI'd.

Copy link
Member

@marioevz marioevz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing this.

I left a couple of comments that should help clarify how these kinds of test should be written IMO.

@@ -114,6 +114,9 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]):
withdrawals: List[Withdrawal] | None = Field(None)
extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True)

# EIP-7928: Block-level access lists
bal_hash: Hash | None = Field(None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this belongs to theResult returned from the transition tool after executing the state transition.

Environment is more for things that affect the EVM execution and/or are observable in the EVM context.

If we put it in Result, we can catch it in here:

header = FixtureHeader(
**(
transition_tool_output.result.model_dump(
exclude_none=True, exclude={"blob_gas_used", "transactions_trie"}
)
| env.model_dump(exclude_none=True, exclude={"blob_gas_used"})
),
blob_gas_used=blob_gas_used,
transactions_trie=Transaction.list_root(txs),
extra_data=block.extra_data if block.extra_data is not None else b"",
fork=fork,
)

and then even verify it during the test by using header_verify as in this example here.

Copy link
Member Author

@raxhvl raxhvl Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @marioevz I see this differently. The test supplies the following to the client as part of a block:

  1. A list of transactions
  2. SSZ encoded block access list (BAL)
  3. BAL hash

In the PR (1) and (2) is part of a test. (3) is computed by the framework from (2).

State transition

The client's state transition function takes all three to produce a block. In addition to executing the transactions, the client:

  1. Computes its own copy BAL and then
  2. Computes BAL hash

The Client and NOT test verifies hash

The client MUST reject the block if computed hash does not match the provided one. ref: spec

If hash is not supplied as an input to the client it will not be able to perform this check. Hence its inclusion in ENV.

Copy link
Collaborator

@fselmo fselmo Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if I misunderstood anything in this flow.

We are trying to fill tests here to generate a test fixture with a built block to then test against clients. So we are essentially testing the EL's ability to locally build a valid block when we're filling tests. This block is then sent as a payload to a client via the test fixture. So I feel like we could actually perform the check at the Result level here. If we build our BALs and compute our own hash to check against, what we need from the filler (let's say this is EELS) is that they compute their own BALs internally, generate the hash, and we validate that this hash is the expected hash in the result with header_verify=Header(bal_hash=correct_bal_hash). And we can test any invalid cases with rlp_modifier=Header(bal_hash=invalid_bal_hash) and exception=BlockException.INVALID_BLOCK_HASH (or a more appropriate bal hash exception).

The client MUST reject the block if computed hash does not match the provided one. ref: spec

I think this comes into play when we're executing the test payload against a client. They will now have a filled block in the test fixture, with the hash that we validated is correct, and they should indeed correctly raise on an invalid hash according to the spec.

Am I missing anything @raxhvl? Does this go along with what you were thinking @marioevz?

)

# Create block with custom header that includes BAL hash
block = Block(txs=[tx], block_access_lists=bal.serialize())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be more of a verification that the BAL-specific tests do, rather than something you have to pass on every test.

There's two ways of handling BALs in tests IMO:

  1. The test is unconcerned with BALs, so we take the value that was returned from the transition tool for granted and just put it in the result.
  2. The test is meant to verify BALs, so we add the header_verify field when constructing the block for the result from the transition tool to be cross-checked with the one that the test expects.

The BAL will be either way be verified at client level when the test is being executed either in hive or by the client consumer, and the test should fail if the BAL hash does not match what the filled test says.

Taking into account, I think this specific example should be rewritten as something like:

Suggested change
block = Block(txs=[tx], block_access_lists=bal.serialize())
block = Block(txs=[tx], header_verify=Header(bal_hash=bal.serialize().hash()))

Assuming bal.serialize().hash() returns the hash that should be placed in the header.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing the discussion, client simply rejects a block with invalid hash. For the test, client behaves like a black box which takes all three inputs to either accept or reject the block.

Copy link
Collaborator

@fselmo fselmo Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just getting to this comment now but this agrees with the way I'm thinking about it as well as I described above.

The client will build its own BAL and compute its own hash. We do so internally in the test and will need validate that it's correct after filling.

So what we need is a built BAL that can be hashed in the appropriate way (this can be bal.hash, bal.serialize().hash(), or however we want to wire it) and we need to check here that the hash is as expected. When the client runs this filled test against it, the payload from the fixture is built correctly (or if invalid will have an expect_exception) and this will test that the client independently builds its own BAL and hashes appropriately.

Copy link
Collaborator

@fselmo fselmo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass here @raxhvl but I want to make sure I'm on the right track. If you can link the pokebal dependency that would be great. I also think we should adjust the naming block_access_lists (anywhere this is found) -> block_access_list (EIP syntax).

@@ -114,6 +114,9 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]):
withdrawals: List[Withdrawal] | None = Field(None)
extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True)

# EIP-7928: Block-level access lists
bal_hash: Hash | None = Field(None)
Copy link
Collaborator

@fselmo fselmo Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if I misunderstood anything in this flow.

We are trying to fill tests here to generate a test fixture with a built block to then test against clients. So we are essentially testing the EL's ability to locally build a valid block when we're filling tests. This block is then sent as a payload to a client via the test fixture. So I feel like we could actually perform the check at the Result level here. If we build our BALs and compute our own hash to check against, what we need from the filler (let's say this is EELS) is that they compute their own BALs internally, generate the hash, and we validate that this hash is the expected hash in the result with header_verify=Header(bal_hash=correct_bal_hash). And we can test any invalid cases with rlp_modifier=Header(bal_hash=invalid_bal_hash) and exception=BlockException.INVALID_BLOCK_HASH (or a more appropriate bal hash exception).

The client MUST reject the block if computed hash does not match the provided one. ref: spec

I think this comes into play when we're executing the test payload against a client. They will now have a filled block in the test fixture, with the hash that we validated is correct, and they should indeed correctly raise on an invalid hash according to the spec.

Am I missing anything @raxhvl? Does this go along with what you were thinking @marioevz?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope:tools Scope: Changes ethereum_test_tools package
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants